from copy     import deepcopy
from re       import match  as re_match
from textwrap import fill as textwrap_fill
from .utils      import RTOL, ATOL
from .utils      import equals as utils_equals
from .units      import Units
from .comparison import Comparison
from .data       import Data

# ====================================================================
#
# Variable object
#
# ====================================================================

class Variable(object):
    '''

Base class for storing a data array with metadata.

A variable contains a data array and metadata comprising properties to
describe the physical nature of the data.

All components of a variable are optional.

'''
    # Define the reserved attributes. These are methods which can't be
    # overwritten, as well as a few attributes.
    _reserved_attrs = ('_reserved_attrs',
                       '_insert_data'
                       '_getter',
                       '_setter',
                       '_deleter',
                       '_set_Data_attributes',
                       'binary_mask',
                       'chunk',
                       'clip',
                       'copy',
                       'cos',
                       'delprop',
                       'dump',
                       'equals',
                       'expand_dims',
                       'flip',
                       'getprop',
                       'hasprop',
                       'identity',
                       'match',
                       'name',
                       'override_units',
                       'setitem',
                       'setmask',
                       'setprop',
                       'sin',
                       'subset',
                       'subspace',
                       'transpose',
                       )

    _special_attributes = set(('units',
                               'calendar',
                               '_FillValue',
                               ))

    def __init__(self, properties={}):
        '''

**Initialization**

:Parameters:

    properties : dict, optional
        Initialize a new instance with CF properties from the
        dictionary's key/value pairs. Values are deep copied.

'''
        # Initialize the _private dictionary with an empty Units
        # object
        self._private = {'special_attributes': {'Units'     : Units(),
                                                '_FillValue': None},
                         'simple_attributes' : {},
                         }

        for prop, value in properties.iteritems():
            self.setprop(prop, deepcopy(value))
    #--- End: def

    def __setattr__(self, attr, value):
        '''
x.__setattr__(attr, value) <==> x.attr=value

'''
        if attr in self._reserved_attrs:
            raise AttributeError("Can't set %s reserved attribute '%s'" %
                                 (self.__class__.__name__, attr))

        super(Variable, self).__setattr__(attr, value)
    #--- End: def

    def __delattr__(self, attr):
        '''
x.__delattr__(attr) <==> del x.attr

'''
        if attr in self._reserved_attrs:
            raise AttributeError("Can't delete reserved attribute '%s'" % attr)

        super(Variable, self).__delattr__(attr)
    #--- End: def

    def __getitem__(self, index):
        '''
x.__getitem__(index) <==> x[index]

'''
        if isinstance(index, slice):
            if not (index == slice(None)                                       or
                    (index.start ==  0 and index.stop == None)                 or
                    (index.start == -1 and index.stop == None)                 or
                    (index.start ==  0 and index.stop == 1 and index.step > 0)):
                raise IndexError("%s index out of range: %s" %
                                 (self.__class__.__name__, index))
        else:
            if index not in (0, -1):
                raise IndexError("%s index out of range: %s" %
                                 (self.__class__.__name__, index))
        #--- End: if

        return self
    #--- End: def

    def __setitem__(self, index, value):
        '''
x.__setitem__(index, value) <==> x[index]=value

'''
        pass # for now! DCH
    #--- End: def

    def __len__(self):
        '''
x.__len__() <==> len(x)

'''
        return 1
    #--- End: def

    def __deepcopy__(self, memo):
        '''

Used if copy.deepcopy is called on the variable.

'''
        return self.copy()
    #--- End: def

    def _binary_operation(self, other, method):
        '''

Implement binary arithmetic and comparison operations on the
Variable's Data with the numpy broadcasting rules. It is intended to
be called by the binary arithmetic and comparison methods, such as
__sub__() and __lt__().

:Parameters:

    operation : str
        The binary arithmetic or comparison method name (such as
        "__imul__" or "__ge__").

:Returns:

    new : Variable
        A new Variable, or the same Variable if the operation was
        in-place.

**Examples**

>>> u = cf.Data([0, 1, 2, 3])
>>> v = cf.Data([1, 1, 3, 4])

>>> w = u._binary_operation(u, '__add__')
>>> print w.array
[1 2 5 7]

>>> w = u._binary_operation(v, '__lt__')
>>> print w.array
[ True  False  True  True]

>>> u._binary_operation(2, '__imul__')
>>> print u.array
[0 2 4 6]

'''
        if not self.hasData:
            raise ValueError(
                "Can't use a %s with no Data in %s operation" %
                (self.__class__.__name__, method))

        inplace = method[2] == 'i'

        if hasattr(other, 'Data'):
            other = other.Data

        if not inplace:
            new      = self.copy()
            new.Data = getattr(self.Data, method)(other)

            #if not new.Data.Units.equivalent(original_units):
            #    # this is coarse!
            #    new.delprop('standard_name')
            #
            #    if hasattr(new, 'history'):
            #        history = [new.getprop('history')]
            #    else:
            #        history = []
            #
            #    history.append(new.getprop('standard_name'))
            #    history.append(method)
            #
            #    new.setprop('history', ' '.join(history))
            ##--- End: if

            return new

        else:
            getattr(self.Data, method)(other)
            return self
    #--- End: def

    def _unary_operation(self, method):
        '''

Implement unary arithmetic operations on the data array.

:Parameters:

    method : str
        The unary arithmetic method name (such as "__abs__").

:Returns:

    new : Variable
        A new Variable.

**Examples**

>>> type(v)
<CF cf.Variable>
>>> print v.array
[1 2 -3 -4 -5]

>>> w = v._unary_operation('__abs__')
>>> print w.array
[1 2 3 4 5]

>>> w = v.__abs__()
>>> print w.array
[1 2 3 4 5]

>>> w = abs(v)
>>> print w.array
[1 2 3 4 5]

'''
        if not self.hasData:
            raise ValueError(
                "Can't apply unary operator %s to a %s with no Data" %                
                (method, self.__class__.__name__))

        new      = self.copy()
        new.Data = getattr(new.Data, method)()

        return new
    #--- End: def

    def __add__(self, other):
        '''
x.__add__(y) <==> x+y

'''
        return self._binary_operation(other, '__add__')
    #--- End: def

    def __iadd__(self, other):
        '''
x.__iadd__(y) <==> x+=y

'''
        return self._binary_operation(other, '__iadd__')
    #--- End: def

    def __radd__(self, other):
        '''
x.__radd__(y) <==> y+x

'''
        return self._binary_operation(other, '__radd__')
    #--- End: def

    def __sub__(self, other):
        '''
x.__sub__(y) <==> x-y

'''
        return self._binary_operation(other, '__sub__')
    #--- End: def

    def __isub__(self, other):
        '''
x.__isub__(y) <==> x-=y

'''
        return self._binary_operation(other, '__isub__')
    #--- End: def

    def __rsub__(self, other):
        '''
x.__rsub__(y) <==> y-x

'''
        return self._binary_operation(other, '__rsub__')
    #--- End: def

    def __mul__(self, other):
        '''
x.__mul__(y) <==> x*y

'''
        return self._binary_operation(other, '__mul__')
    #--- End: def

    def __imul__(self, other):
        '''
x.__imul__(y) <==> x*=y

'''
        return self._binary_operation(other, '__imul__')
    #--- End: def

    def __rmul__(self, other):
        '''
x.__rmul__(y) <==> y*x

'''
        return self._binary_operation(other, '__rmul__')
    #--- End: def

    def __div__(self, other):
        '''
x.__div__(y) <==> x/y

'''
        return self._binary_operation(other, '__div__')
    #--- End: def

    def __idiv__(self, other):
        '''
x.__idiv__(y) <==> x/=y

'''
        return self._binary_operation(other, '__idiv__')
    #--- End: def

    def __rdiv__(self, other):
        '''
x.__rdiv__(y) <==> y/x

'''
        return self._binary_operation(other, '__rdiv__')
    #--- End: def

    def __floordiv__(self, other):
        '''
x.__floordiv__(y) <==> x//y

'''
        return self._binary_operation(other, '__floordiv__')
    #--- End: def

    def __ifloordiv__(self, other):
        '''
x.__ifloordiv__(y) <==> x//=y

'''
        return self._binary_operation(other, '__ifloordiv__')
    #--- End: def

    def __rfloordiv__(self, other):
        '''
x.__rfloordiv__(y) <==> y//x

'''
        return self._binary_operation(other, '__rfloordiv__')
    #--- End: def

    def __truediv__(self, other):
        '''
x.__truediv__(y) <==> x/y

'''
        return self._binary_operation(other, '__truediv__')
    #--- End: def

    def __itruediv__(self, other):
        '''
x.__itruediv__(y) <==> x/=y

'''
        return self._binary_operation(other, '__itruediv__')
   #--- End: def

    def __rtruediv__(self, other):
        '''
x.__rtruediv__(y) <==> y/x

'''
        return self._binary_operation(other, '__rtruediv__')
    #--- End: def

    def __pow__(self, other, modulo=None):
        '''
x.__pow__(y) <==> x**y

'''
        if modulo is not None:
            raise NotImplementedError(
                "3-argument power not supported for '%s'" %
                self.__class__.__name__)
        
        return self._binary_operation(other, '__pow__')
    #--- End: def

    def __ipow__(self, other, modulo=None):
        '''
x.__ipow__(y) <==> x**=y

'''
        if modulo is not None:
            raise NotImplementedError(
                "3-argument power not supported for '%s'" %
                self.__class__.__name__)

        return self._binary_operation(other, '__ipow__')
    #--- End: def

    def __rpow__(self, other, modulo=None):
        '''
x.__ipow__(y) <==> x**=y

'''
        if modulo is not None:
            raise NotImplementedError(
                "3-argument power not supported for '%s'" %
                self.__class__.__name__)

        return self._binary_operation(other, '__rpow__')
    #--- End: def

    def __eq__(self, other):
        '''
x.__eq__(y) <==> x==y

'''
        return self._binary_operation(other, '__eq__')
    #--- End: def

    def __ne__(self, other):
        '''
x.__ne__(y) <==> x!=y

'''
        return self._binary_operation(other, '__ne__')
    #--- End: def

    def __ge__(self, other):
        '''
x.__ge__(y) <==> x>=y

'''
        return self._binary_operation(other, '__ge__')
    #--- End: def

    def __gt__(self, other):
        '''
x.__gt__(y) <==> x>y

'''
        return self._binary_operation(other, '__gt__')
    #--- End: def

    def __le__(self, other):
        '''
x.__le__(y) <==> x<=y

'''
        return self._binary_operation(other, '__le__')
    #--- End: def

    def __lt__(self, other):
        '''
x.__lt__(y) <==> x<y

'''
        return self._binary_operation(other, '__lt__')
    #--- End: def

    def __and__(self, other):
        '''
x.__and__(y) <==> x&y

'''
        return self._binary_operation(other, '__and__')
    #--- End: def

    def __iand__(self, other):
        '''
x.__iand__(y) <==> x&=y

'''
        return self._binary_operation(other, '__ior__')
    #--- End: def

    def __or__(self, other):
        '''
x.__or__(y) <==> x|y

'''
        return self._binary_operation(other, '__or__')
    #--- End: def

    def __ior__(self, other):
        '''
x.__ior__(y) <==> x|=y

'''
        return self._binary_operation(other, '__ior__')
    #--- End: def

    def __abs__(self):
        '''
x.__abs__() <==> abs(x)

'''
        return self._unary_operation('__abs__')
    #--- End: def

    def __neg__(self):
        '''
x.__neg__() <==> -x

'''
        return self._unary_operation('__neg__')
    #--- End: def

    def __invert__(self):
        '''
x.__invert__() <==> ~x

'''
        return self._unary_operation('__invert__')
    #--- End: def

    def __pos__(self):
        '''
x.__pos__() <==> +x

'''
        return self._unary_operation('__pos__')
    #--- End: def

    def __repr__(self):
        '''
x.__repr__() <==> repr(x)

'''
        name = self.name(default='', long_name=True, ncvar=True)

        if hasattr(self, 'shape'):
            shape = str(self.shape).replace(',)', ')', 1)
            return '<CF %s: %s%s>' % (self.__class__.__name__, name, shape)
        else:
            return '<CF %s: %s>' % (self.__class__.__name__, name)
    #--- End: def

    def __str__(self):
        '''

x.__str__() <==> str(x)

'''
        return self.__repr__()
    #--- End: def

    def _set_Data_attributes(self, dimensions, directions):
        '''

Set the data metadata.

:Parameters:

    dimensions : list

    directions : dict

:Returns:

    None

'''
        data = self.Data

        # ----------------------------------------------------------------
        # Dimension names
        # ----------------------------------------------------------------
        dim_name_map = dict((dim, new_dim)
                            for dim, new_dim in zip(data.order, dimensions))
        data.change_dimension_names(dim_name_map)

        # ----------------------------------------------------------------
        # Dimension directions
        # ----------------------------------------------------------------
        if data.isscalar:
            # Scalar data
            if dimensions:
                data.direction = directions[dimensions[0]]
            else:
                data.direction = True

            for partition in data.partitions.flat():
                partition.direction = data.direction
        else:
            # 1+ dimensional data
            direction = data.direction
            for dim in direction:
                direction[dim] = directions[dim]

            for partition in data.partitions.flat():
                partition.direction = direction.copy()
        #--- End: if
    #--- End: def

    def _insert_data(self, other, indices,
                     PDim, PDim_direction, dim_name_map):
        '''

'''
        self.Data._insert_data(other.Data, indices,
                               PDim, PDim_direction, dim_name_map)
    #--- End: def

    def _get_special_attr(self, attr):
        '''

'''
        if attr in self._private['special_attributes']:
            return self._private['special_attributes'][attr]

        raise AttributeError("%s doesn't have attribute '%s'" %
                             (self.__class__.__name__, attr))
    #--- End: def

    def _set_special_attr(self, attr, value):
        '''

'''
        self._private['special_attributes'][attr] = value
    #--- End: def

    def _del_special_attr(self, attr):
        '''

'''
        if attr in self._private['special_attributes']:
            del self._private['special_attributes'][attr]
        else:
            raise AttributeError(
                "%s doesn't have attribute '%s'" %
                (self.__class__.__name__, attr))
    #--- End: def

    def _getter(self, attr):
        '''

Get an attribute from the _private dictionary.

'''
        if attr in self._private:
            return self._private[attr]

        raise AttributeError("Can't get '%s' attribute '%s'" %
                             (self.__class__.__name__, attr))
    #--- End: def

    def _setter(self, attr, value):
        '''

Set an attribute in the _private dictionary.

'''
        self._private[attr] = value
    #--- End: def

    def _deleter(self, attr):
        '''

Deleting an attribute from the _private dictionary.

'''
        if attr in self._private:
            del self._private[attr]
        else:
            raise AttributeError("Can't delete '%s' attribute '%s'" %
                                 (self.__class__.__name__, attr))
    #--- End: def

    def _equivalent_data(self, other, transpose=None,
                         rtol=None, atol=None):
        '''

:Returns:

    out : bool
        Whether or not the two variables have equivalent data arrays.

'''
        if not (self.hasData and other.hasData):
            return False

        return self.Data._equivalent(other.Data,
                                     rtol=rtol, atol=atol,
                                     transpose=transpose,
                                     squeeze=False,
                                     use_directions=True)
    #--- End: def

    # ----------------------------------------------------------------
    # Attribute: Data
    # ----------------------------------------------------------------
    @property
    def Data(self):
        '''

The `Data` object containing the data array.

**Examples**

>>> f.Data
<CF Data: >

'''
        if 'Data' in self._private:
            return self._private['Data']

        raise AttributeError("%s doesn't have attribute 'Data'" %
                             self.__class__.__name__)
    #--- End: def
    @Data.setter
    def Data(self, value):
        self._private['Data'] = value

        # Delete from the variable, if they had been set
        special_attributes = self._private['special_attributes']

        if 'Units' in special_attributes:
            special_attributes.pop('Units')

        if '_FillValue' in special_attributes:
            special_attributes.pop('_FillValue')
    #--- End: def

    @Data.deleter
    def Data(self):
        if not self.hasData:
            raise AttributeError("%s object doesn't have attribute 'Data'" %
                                 self.__class__.__name__)
        #--- End: if

        # Save the Units and _FillValue attributes before deletion
        special_attributes = self._private['special_attributes']
        special_attributes['Units']      = self.Data.Units
        special_attributes['_FillValue'] = self.Data._FillValue
        
        del self._private['Data']
    #--- End: def

    # ----------------------------------------------------------------
    # Attribute: Units
    # ----------------------------------------------------------------
    @property
    def Units(self):
        '''

The `Units` object containing the units of the data array.

Stores the units and calendar CF properties in an internally
consistent manner. These are mirrored by the `units` and `calendar` CF
properties respectively.

**Examples**

>>> f.Units
<CF Units: K>

'''
        if self.hasData:
            return self.Data.Units
        else:
            return self._get_special_attr('Units')
    #--- End: def

    @Units.setter
    def Units(self, value):
        if not isinstance(value, Units):
            raise TypeError(
                "'%s' attribute 'Units' must be a 'Units' object (got '%s')" %
                (self.__class__.__name__, value.__class__.__name__))
        #--- End: if

        if self.hasData:
            self.Data.Units = value
        else:
            self._set_special_attr('Units', value)
     #--- End: def

    @Units.deleter
    def Units(self):
        raise AttributeError("Won't delete %s attribute 'Units'" %
                             self.__class__.__name__)
    #--- End: def

    # ----------------------------------------------------------------
    # CF property: add_offset
    # ----------------------------------------------------------------
    @property
    def add_offset(self):
        '''

The add_offset CF property.

This property is only used when writing to a file on disk.

**Examples**

>>> f.add_offset = -4.0
>>> f.add_offset
-4.0
>>> del f.add_offset

>>> f.setprop('add_offset', 10.5)
>>> f.getprop('add_offset')
10.5
>>> f.delprop('add_offset')

'''
        return self.getprop('add_offset')
    #--- End: def
    @add_offset.setter
    def add_offset(self, value): self.setprop('add_offset', value)
    @add_offset.deleter
    def add_offset(self):        self.delprop('add_offset')

    # ----------------------------------------------------------------
    # CF property: calendar
    # ----------------------------------------------------------------
    @property
    def calendar(self):
        '''

The calendar CF property.

This property is a mirror of the calendar stored in the `Units`
attribute.

**Examples**

>>> f.calendar = 'noleap'
>>> f.calendar
'noleap'
>>> del f.calendar

>>> f.setprop('calendar', 'proleptic_gregorian')
>>> f.getprop('calendar')
'proleptic_gregorian'
>>> f.delprop('calendar')

'''
        if hasattr(self.Units, 'calendar'):
            return self.Units.calendar

        raise AttributeError("%s doesn't have CF property 'calendar'" %
                             self.__class__.__name__)
    #--- End: def

    @calendar.setter
    def calendar(self, value):
        self.Units.calendar = value
    #--- End: def

    @calendar.deleter
    def calendar(self):
        if hasattr(self.Units, 'calendar'):
            del self.Units.calendar
        else:
            raise AttributeError("%s doesn't have CF property 'calendar'" %
                                 self.__class__.__name__)
    #--- End: def

    # ----------------------------------------------------------------
    # CF property: comment (a simple attribute)
    # ----------------------------------------------------------------
    @property
    def comment(self):
        '''

The comment CF property.

**Examples**

>>> f.comment = 'a comment'
>>> f.comment
'a comment'
>>> del f.comment

>>> f.setprop('comment', 'a comment')
>>> f.getprop('comment')
'a comment'
>>> f.delprop('comment')

'''
        return self.getprop('comment')
    #--- End: def
    @comment.setter
    def comment(self, value): self.setprop('comment', value)
    @comment.deleter
    def comment(self):        self.delprop('comment')

    # ----------------------------------------------------------------
    # CF property: _FillValue
    # ----------------------------------------------------------------
    @property
    def _FillValue(self):
        '''

The _FillValue CF property.

This property is forced to be consistent with the `missing_value`
property as follows:

    * Assigning a value to `missing_value` also assigns the same value
      to the `_FillValue` whether the latter has been previously set
      or not.

    * Assigning a value to `_FillValue` also assigns the same value to
      `missing_value`, but only if the latter has previously been
      defined.

**Examples**

>>> f._FillValue = -1e30

'''
        if self.hasData:
            return self.Data._FillValue
        else:
           return self._get_special_attr('_FillValue')
    #--- End: def

    @_FillValue.setter
    def _FillValue(self, value):
        if self.hasData:
            self.Data._FillValue = value
        else:
            self._set_special_attr('_FillValue', value)

        if hasattr(self, 'missing_value'):
            if value is None:
                del self._private['simple_attributes']['missing_value']
            else:
                self._private['simple_attributes']['missing_value'] = value
    #--- End: def

    @_FillValue.deleter
    def _FillValue(self):
        if self.hasData:
            del self.Data._FillValue
        else:
            self._del_special_attr('_FillValue')

        if hasattr(self, 'missing_value'):
            del self.missing_value
    #--- End: def

    # ----------------------------------------------------------------
    # CF property: history	
    # ----------------------------------------------------------------
    @property
    def history(self):
        '''

The history CF property.

**Examples**

>>> f.history = 'created on 2012/10/01'
>>> f.history
'created on 2012/10/01'
>>> del f.history

>>> f.setprop('history', 'created on 2012/10/01')
>>> f.getprop('history')
'created on 2012/10/01'
>>> f.delprop('history')

'''
        return self.getprop('history')
    #--- End: def

    @history.setter
    def history(self, value): self.setprop('history', value)
    @history.deleter
    def history(self):        self.delprop('history')

    # ----------------------------------------------------------------
    # CF property: leap_month (a simple attribute)
    # ----------------------------------------------------------------
    @property
    def leap_month(self):
        '''

The leap_month CF property.

**Examples**

>>> f.leap_month = 2
>>> f.leap_month
2
>>> del f.leap_month

>>> f.setprop('leap_month', 2)
>>> f.getprop('leap_month')
2
>>> f.delprop('leap_month')

'''
        return self.getprop('leap_month')
    #--- End: def
    @leap_month.setter
    def leap_month(self, value): self.setprop('leap_month', value)
    @leap_month.deleter
    def leap_month(self):        self.delprop('leap_month')

    # ----------------------------------------------------------------
    # CF property: leap_year (a simple attribute)
    # ----------------------------------------------------------------
    @property
    def leap_year(self):
        '''

The leap_year CF property.

**Examples**

>>> f.leap_year = 1984
>>> f.leap_year
1984
>>> del f.leap_year

>>> f.setprop('leap_year', 1984)
>>> f.getprop('leap_year')
1984
>>> f.delprop('leap_year')

'''
        return self.getprop('leap_year')
    #--- End: def
    @leap_year.setter
    def leap_year(self, value): self.setprop('leap_year', value)
    @leap_year.deleter
    def leap_year(self):        self.delprop('leap_year')

    # ----------------------------------------------------------------
    # CF property: long_name
    # ----------------------------------------------------------------
    @property
    def long_name(self):
        '''

The long_name CF property.

**Examples**

>>> f.long_name = 'zonal_wind'
>>> f.long_name
'zonal_wind'
>>> del f.long_name

>>> f.setprop('long_name', 'surface air temperature')
>>> f.getprop('long_name')
'surface air temperature'
>>> f.delprop('long_name')

'''
        return self.getprop('long_name')
    #--- End: def
    @long_name.setter
    def long_name(self, value): self.setprop('long_name', value)
    @long_name.deleter
    def long_name(self):        self.delprop('long_name')

    # ----------------------------------------------------------------
    # CF property: missing_value
    # ----------------------------------------------------------------
    @property
    def missing_value(self):
        '''

The missing_value CF property.

This property is forced to be consistent with the `_FillValue`
property as follows:

    * Assigning a value to `missing_value` also assigns the same value
      to `_FillValue` whether the latter has been previously set or
      not.

    * Assigning a value to `_FillValue` also assigns the same value to
      `missing_value`, but only if the latter has previously been
      defined.

**Examples**

>>> f.missing_value = 1e30
>>> f.missing_value
1e30
>>> f._FillValue
1e30
>>> del f.missing_value
>>> f._FillValue
1e30

'''        
        return self.getprop('missing_value')
    #--- End: def
    @missing_value.setter
    def missing_value(self, value): self.setprop('missing_value', value)
    #--- End: def
    @missing_value.deleter
    def missing_value(self):        self.delprop('missing_value')
    #--- End: def

    # ----------------------------------------------------------------
    # CF property: month_lengths (a simple attribute)
    # ----------------------------------------------------------------
    @property
    def month_lengths(self):
        '''

The month_lengths CF property.

Stored as a tuple but may be set as any array-like object.

**Examples**

>>> f.month_lengths = numpy.array([34, 31, 32, 30, 29, 27, 28, 28, 28, 32, 32, 34])
>>> f.month_lengths
(34, 31, 32, 30, 29, 27, 28, 28, 28, 32, 32, 34)
>>> del f.month_lengths

>>> f.setprop('month_lengths', [34, 31, 32, 30, 29, 27, 28, 28, 28, 32, 32, 34])
>>> f.getprop('month_lengths')
(34, 31, 32, 30, 29, 27, 28, 28, 28, 32, 32, 34)
>>> f.delprop('month_lengths')

'''
        return self.getprop('month_lengths')
    #--- End: def

    @month_lengths.setter
    def month_lengths(self, value): self.setprop('month_lengths', tuple(value))
    @month_lengths.deleter
    def month_lengths(self):        self.delprop('month_lengths')

    # ----------------------------------------------------------------
    # CF property: scale_factor
    # ----------------------------------------------------------------
    @property
    def scale_factor(self):
        '''

The scale_factor CF property.

This property is only used when writing to a file on disk.

**Examples**

>>> f.scale_factor = 10.0
>>> f.scale_factor
10.0
>>> del f.scale_factor

>>> f.setprop('scale_factor', 10.0)
>>> f.getprop('scale_factor')
10.0
>>> f.delprop('scale_factor')

'''
        return self.getprop('scale_factor')
    #--- End: def
    @scale_factor.setter
    def scale_factor(self, value): self.setprop('scale_factor', value)
    @scale_factor.deleter
    def scale_factor(self):        self.delprop('scale_factor')

    # ----------------------------------------------------------------
    # CF property: standard_name
    # ----------------------------------------------------------------
    @property
    def standard_name(self):
        '''

The standard_name CF property.

**Examples**

>>> f.standard_name = 'time'
>>> f.standard_name
'time'
>>> del f.standard_name

>>> f.setprop('standard_name', 'time')
>>> f.getprop('standard_name')
'time'
>>> f.delprop('standard_name')

'''
        return self.getprop('standard_name')
    #--- End: def
    @standard_name.setter
    def standard_name(self, value): self.setprop('standard_name', value)
    @standard_name.deleter
    def standard_name(self):        self.delprop('standard_name')

    # ----------------------------------------------------------------
    # CF property: units
    # ----------------------------------------------------------------
    @property
    def units(self):
        '''

The units CF property.

This property is a mirror of the units stored in the `Units`
attribute.

**Examples**

>>> f.units = 'K'
>>> f.units
'K'
>>> del f.units

>>> f.setprop('units', 'm.s-1')
>>> f.getprop('units')
'm.s-1'
>>> f.delprop('units')

'''
        if hasattr(self.Units, 'units'):
            return self.Units.units

        raise AttributeError("%s doesn't have CF property 'units'" %
                             self.__class__.__name__)
    #--- End: def

    @units.setter
    def units(self, value):
        self.Units.units = value
    #--- End: def
    @units.deleter
    def units(self):
        if hasattr(self.Units, 'units'):
            del self.Units.units
        else:
            raise AttributeError("%s doesn't have CF property 'units'" %
                                 self.__class__.__name__)
    #--- End: def

    # ----------------------------------------------------------------
    # CF property: valid_max
    # ----------------------------------------------------------------
    @property
    def valid_max(self):
        '''

The valid_max CF property.

**Examples**

>>> f.valid_max = 100.0
>>> f.valid_max
100.0
>>> del f.valid_max

>>> f.setprop('valid_max', 100.0)
>>> f.getprop('valid_max')
100.0
>>> f.delprop('valid_max')

'''
        return self.getprop('valid_max')
    #--- End: def
    @valid_max.setter
    def valid_max(self, value): self.setprop('valid_max', value)
    @valid_max.deleter
    def valid_max(self):        self.delprop('valid_max')

    # ----------------------------------------------------------------
    # CF property: valid_min
    # ----------------------------------------------------------------
    @property
    def valid_min(self):
        '''

The valid_min CF property.

**Examples**

>>> f.valid_min = 100.0
>>> f.valid_min
100.0
>>> del f.valid_min

>>> f.setprop('valid_min', 100.0)
>>> f.getprop('valid_min')
100.0
>>> f.delprop('valid_min')

'''
        return self.getprop('valid_min')
    #--- End: def
    @valid_min.setter
    def valid_min(self, value): self.setprop('valid_min', value)
    @valid_min.deleter
    def valid_min(self):        self.delprop('valid_min')

    # ----------------------------------------------------------------
    # CF property: valid_range
    # ----------------------------------------------------------------
    @property
    def valid_range(self):
        '''

The valid_range CF property.

Stored as a tuple but may be set as any array-like object.

**Examples**

>>> f.valid_range = numpy.array([100., 400.])
>>> f.valid_range
(100.0, 400.0)
>>> del f.valid_range

>>> f.setprop('valid_range', [100.0, 400.0])
>>> f.getprop('valid_range')
(100.0, 400.0)
>>> f.delprop('valid_range')

'''
        return tuple(self.getprop('valid_range'))
    #--- End: def
    @valid_range.setter
    def valid_range(self, value): self.setprop('valid_range', tuple(value))
    @valid_range.deleter
    def valid_range(self):        self.delprop('valid_range')

    @property
    def subspace(self):
        '''

Return a new variable whose data is subspaced.

This attribute may be indexed to select a subspace from dimension
index values.

**Subspacing by indexing**

Subspacing by dimension indices uses an extended Python slicing
syntax, which is similar numpy array indexing. There are two
extensions to the numpy indexing functionality:

* Size 1 dimensions are never removed.

  An integer index i takes the i-th element but does not reduce the
  rank of the output array by one.

* When advanced indexing is used on more than one dimension, the
  advanced indices work independently.

  When more than one dimension's slice is a 1-d boolean array or 1-d
  sequence of integers, then these indices work independently along
  each dimension (similar to the way vector subscripts work in
  Fortran), rather than by their elements.

**Examples**

'''
        return SubspaceVariable(self)
    #--- End: def

    # ----------------------------------------------------------------
    # Attribute: shape (read only)
    # ----------------------------------------------------------------
    @property
    def shape(self):
        '''

Tuple of the data array's dimension sizes.

**Examples**

>>> f.shape
(73, 96)

'''
        if self.hasData:
            return tuple(self.Data.shape)

        raise AttributeError("%s doesn't have attribute 'shape'" %
                             self.__class__.__name__)
    #--- End: def

    # ----------------------------------------------------------------
    # Attribute: ndim (read only)
    # ----------------------------------------------------------------
    @property
    def ndim(self):
        '''

Number of dimensions in the data array.

**Examples**

>>> f.shape
(73, 96)
>>> f.ndim
2

'''
        if self.hasData:
            return self.Data.ndim

        raise AttributeError("%s doesn't have attribute 'ndim'" %
                             self.__class__.__name__)
    #--- End: def

    # ----------------------------------------------------------------
    # Attribute: size (read only)
    # ----------------------------------------------------------------
    @property
    def size(self):
        '''

Number of elements in the data array.

**Examples**

>>> f.shape
(73, 96)
>>> f.size
7008

'''
        if self.hasData:
            return self.Data.size
        
        raise AttributeError("%s doesn't have attribute 'size'" %
                             self.__class__.__name__)
    #--- End: def

    # ----------------------------------------------------------------
    # Attribute: dtype
    # ----------------------------------------------------------------
    @property
    def dtype(self):
        '''

The numpy data type of the data array.

By default this is the data type with the smallest size and smallest
scalar kind to which all data array partitions may be safely cast. For
example, if the partitions have data types 'int64' and 'float32' then
the data array's data type will be 'float64'.

Setting the data type to a numpy.dtype object, or any object
convertible to a numpy.dtype object, will change the interpretation of
the underlying data array elements. Note that the underlying data are
not altered, so reinstating the original data type results in no loss
of information.

Deleting the data type after setting it will reinstate the default
behaviour. Deleting the data type when the default behaviour is in
place will have no effect.

**Examples**

>>> f.dtype
dtype('float64')
>>> type(f.dtype)
<type 'numpy.dtype'>
>>> print f.array
[0.5 1.5 2.5]

>>> print f.array
[0.5 1.5 2.5]
>>> import numpy
>>> f.dtype = numpy.dtype(int)
>>> print f.array
[0 1 2]
>>> f.dtype = bool
>>> print f.array
[False  True  True]
>>> f.dtype = 'float64'
>>> print f.array
[0.5 1.5 2.5]

'''
        if self.hasData:
            return self.Data.dtype

        raise AttributeError("%s doesn't have attribute 'dtype'" %
                             self.__class__.__name__)
    #--- End: def
    @dtype.setter
    def dtype(self, value):
        if self.hasData:
            self.Data.dtype = value
    #--- End: def
    @dtype.deleter
    def dtype(self):
        if self.hasData:
            del self.Data.dtype
    #--- End: def

    # ----------------------------------------------------------------
    # Attribute: hardmask
    # ----------------------------------------------------------------
    @property
    def hardmask(self):
        '''

Whether the mask is hard (True) or soft (False).

When the mask is hard, masked entries of the data array can not be
unmasked by assignment.

By default, the mask is hard.

**Examples**

>>> f.hardmask = False
>>> f.hardmask
False

'''
        if self.hasData:
            return self.Data.hardmask

        raise AttributeError("%s doesn't have attribute 'hardmask'" %
                             self.__class__.__name__)
    #--- End: def
    @hardmask.setter
    def hardmask(self, value):
        if self.hasData:
            self.Data.hardmask = value
        else:
            raise AttributeError("%s doesn't have attribute 'hardmask'" %
                                 self.__class__.__name__)
    #--- End: def
    @hardmask.deleter
    def hardmask(self):
        raise AttributeError("Won't delete %s attribute 'hardmask'" %
                             self.__class__.__name__)
    #--- End: def

    @property
    def array(self):
        '''

A numpy array deep copy of the data array.

Changing the returned numpy array does not change the data array.

**Examples**

>>> a = f.array
>>> type(a)
<type 'numpy.ndarray'>
>>> print a
[0 1 2 3 4]
>>> a[0] = 999
>>> print a
[999 1 2 3 4]
>>> print f.array
[0 1 2 3 4]

'''
        if self.hasData:
            return self.Data.array

        raise AttributeError("%s has no Data'" % self.__class__.__name__)
    #--- End: def

    @property
    def varray(self):
        '''

A numpy array view of the data array.

Changing the elements of the returned view changes the data array.

**Examples**

>>> a = f.varray
>>> type(a)
<type 'numpy.ndarray'>
>>> a
array([0, 1, 2, 3, 4])
>>> a[0] = 999
>>> f.varray
array([999, 1, 2, 3, 4])

'''
        if self.hasData:
            return self.Data.varray

        raise AttributeError("%s has no Data'" % self.__class__.__name__)
    #--- End: def

#    @property
#    def tarray(self):
#        if not self.istime():
#            raise TypeError("Can't create a 'Time' array from a non-time variable")
#
#        try:
#            calendar = self.calendar
#        except AttributeError:
#            calendar = 'standard'
#
#        return cf.Time(self.varray, units=self.units, calendar=calendar)
#    #--- End: def

#    def set_time(self, cftime):
#        if not self.istime():
#            raise TypeError("Can't set time for a non-time variable")
#
#        # Check that the Time array has the same size and shape as the
#        # Variable array
#
#        self.units    = cftime.units
#        self.calendar = cftime.calendar
#
#        self[...] = cftime.array
#    #--- End: def

#    def istime(self):
#        '''
#'''
#        try:
#            if ' since ' in self.units:
#                return True
#            return False
#        except AttributeError:
#            return False
#    #--- End: def

#    def change_calendar(self, calendar, fix=False):
#        t = self.tarray
#        t = t.change_calendar(calendar, fix=fix)
#        self.set_time(t)
#    #--- End: def

    # ----------------------------------------------------------------
    # Attribute: first_datum (read only)
    # ----------------------------------------------------------------
    @property
    def first_datum(self):
        '''

The first element of the data array.

**Examples**

>>> print f.array
[[1 2 3 4]]
>>> f.first_datum
1

>>> print f.array
[[-- 2 3 4]]
>>> f.first_datum
--

'''
        return self.Data[(slice(0,1),) * self.ndim].varray.item()
    #--- End: def

    @property
    def hasData(self):
        '''

True if and only if there is a data array.

**Examples**

>>> hasattr(f, 'Data')
True
>>> f.hasData
True
>>> del f.Data
>>> f.hasData
False

'''
        return hasattr(self, 'Data')
    #--- End: if

    @property
    def isscalar(self):
        '''

True if and only if the data array is a scalar array.

**Examples**

>>> print f.array
2
>>> f.isscalar
True

>>> print f.array
[2]
>>> f.isscalar
False


>>> print f.array
[[2, 3]]
>>> f.isscalar
False

>>> f.hasData
False
>>> f.isscalar
False

'''
        if not self.hasData:
            return False

        return self.Data.isscalar
    #--- End: if

    # ----------------------------------------------------------------
    # Attribute: last_datum (read only)
    # ----------------------------------------------------------------
    @property
    def last_datum(self):
        '''

The last element of the data array.

**Examples**

>>> print f.array
[[1 2 3 4]]
>>> f.last_datum
4

>>> print f.array
[[1 2 3 --]]
>>> f.last_datum
--

'''
        return self.Data[(slice(-1,None),) * self.ndim].varray.item()
    #--- End: def

    def chunk(self, chunksize=None, extra_boundaries=[], pdim=[]):
        '''

Partition the data array using Large Amounts of Massive Arrays (LAMA)
functionality.

:Parameters:

    chunksize : int

    extra_boundaries : list

    pdim : list

:Returns:

     extra_boundaries, pdim : {list, list}

'''
        return self.Data.chunk(chunksize, extra_boundaries, pdim)
    #--- End: def

    def clip(self, a_min, a_max, units=None):
        '''

Clip (limit) the values in the data array in place.

Given an interval, values outside the interval are clipped to the
interval edges.

Parameters :
 
    a_min : scalar

    a_max : scalar

    units : str or Units

:Returns: 

    None

**Examples**

'''
        self.Data.clip(a_min, a_max, units=units)
    #--- End: def

    def copy(self, _omit_Data=False, _omit_special=()):
        '''

Return a deep copy.

Equivalent to ``copy.deepcopy(f)``.

:Returns:

    out :
        The deep copy.

**Examples**

>>> g = f.copy()

'''
        new = type(self)()

        if not _omit_Data and self.hasData:
            new.Data = self.Data.copy()

        attrs = set((self._private)) - set(('Data','special_attributes','simple_attributes'))
        for attr in attrs:
            new._private[attr] = deepcopy(self._private[attr])

        attrs = set((self._private['special_attributes'])) - set(_omit_special)
        for attr in attrs:
            new._private['special_attributes'][attr] = \
                deepcopy(self._private['special_attributes'][attr])

        attrs = set((self._private['simple_attributes']))
        for attr in attrs:
            new._private['simple_attributes'][attr] = \
                deepcopy(self._private['simple_attributes'][attr])

        # Attributes
        for attr in set(self.__dict__) - set(('_private',)):
            setattr(new, attr, deepcopy(self.__dict__[attr]))

        return new
    #--- End: def

    def cos(self):
        '''

Take the trigonometric cosine of the data array in place.

Units are accounted for in the calcualtion, so that the the cosine of
90 degrees_east is 0.0, as is the sine of 1.57079632 radians. If the
units are not equivalent to radians (such as Kelvin) then they are
treated as if they were radians.

The Units are changed to '1' (nondimensionsal).

:Returns: 

    None

**Examples**

>>> f.Units
<CF Units: degrees_east>
>>> print f.array
[[-90 0 90 --]]
>>> f.cos()
>>> f.Units
<CF Units: 1>
>>> print f.array
[[0.0 1.0 0.0 --]]

>>> f.Units
<CF Units: m s-1>
>>> print f.array
[[1 2 3 --]]
>>> f.cos()
>>> f.Units
<CF Units: 1>
>>> print f.array
[[0.540302305868 -0.416146836547 -0.9899924966 --]]

'''
        self.Data.cos()
    #--- End: def

    def dump(self, id=None, omit=()):
        '''

Return a string containing a full description of the instance.

:Parameters:

    id : str, optional
       Set the common prefix of component names. By default the
       instance's class name is used.

    omit : sequence of strs
        Omit the given CF properties from the description.

:Returns:

    out : str
        A string containing the description.

**Examples**

>>> x = v.dump()
>>> print v.dump()
>>> print v.dump(id='variable1')
>>> print v.dump(omit=('long_name',))

'''
        if id is None:
            id = self.__class__.__name__

        string = []

        if self.hasData:
            string.append(self.Data.dump(id=id))

        # Special properties
        special = self._private['special_attributes'].copy()
        for attr in omit:
            if attr in special:
                special.pop(attr)
        #--- End: for
        for attr, value in special.iteritems():
            string.append('%s.%s = %s' % (id, attr, repr(value)))

        string.append('')

        # Simple properties
        simple = self._simple_attributes()
        attrs  = sorted(set(simple) - set(omit))
        if attrs:
            for attr in attrs:
                name   = '%(id)s.%(attr)s = ' % locals()
                value  = repr(simple[attr])
                indent = ' ' * (len(name))
                if value.startswith("'") or value.startswith('"'):
                    indent = '%(indent)s ' % locals()
                string.append(textwrap_fill(name+value, 79,
                                            subsequent_indent=indent))
            #--- End: for
            string.append('')
        #--- End: if

        return '\n'.join(string)
    #--- End: def

    def equals(self, other, rtol=None, atol=None, traceback=False,
               ignore=()):
        '''

True if two variables are logically equal, False otherwise.

:Parameters:

    other : 
        The object to compare for equality.

    atol : float, optional
        The absolute tolerance for all numerical comparisons, By
        default the value returned by the `ATOL` function is used.

    rtol : float, optional
        The relative tolerance for all numerical comparisons, By
        default the value returned by the `RTOL` function is used.

    traceback : bool, optional
        If True then print a traceback highlighting where the two
        instances differ.

    ignore : iterable, optional
        Omit these CF properties from the comparison.

:Returns: 

    out : bool
        Whether or not the two instances are equal.

**Examples**

>>> f.equals(f)
True
>>> g = f + 1
>>> f.equals(g)
False
>>> g -= 1
>>> f.equals(g)
True
>>> f.setprop('name', 'name0')
>>> g.setprop('name', 'name1')
>>> f.equals(g)
False
>>> f.equals(g, ignore=set('name',))
True

'''
        # Check each instance's id
        if self is other:
            return True

        # Check that each instance is the same type
        if self.__class__ != other.__class__:
            if traceback:
                print("%s: Different type: %s, %s" %
                      (self.__class__.__name__,
                       self.__class__.__name__,
                       other.__class__.__name__))
            return False
        #--- End: if

        self_simple  = self._private['simple_attributes']
        other_simple = other._private['simple_attributes']
        if (set(self_simple).difference(ignore) != 
            set(other_simple).difference(ignore)):
            if traceback:
                print("%s: Different attributes: %s" % 
                      (self.__class__.__name__,
                       set(self_simple).symmetric_difference(other_simple)))
            return False
        #--- End: if

        if rtol is None:
            rtol = RTOL()
        if atol is None:
            atol = ATOL()

        # Check that each instance has the same CF property values
        for attr, x in self_simple.iteritems():
            if attr in ignore:
                continue
            y = other_simple[attr]
            if not utils_equals(x, y, rtol=rtol, atol=atol, traceback=traceback):
                if traceback:
                    print("%s: Different %s properties: %s, %s" %
                          (self.__class__.__name__, attr, repr(x), repr(y)))
                return False
        #--- End: for

        # Check the special properties which have 'equals' methods,
        # such as Units, etc.
        self_special  = self._private['special_attributes']
        other_special = other._private['special_attributes']
        if set(self_special) != set(other_special):
            if traceback:
                print("%s: Different CF properties: %s" %
                      (self.__class__.__name__,
                       set(self_special).symmetric_difference(other_special)))
            return False
        #--- End: if

        for attr, x in self_special.iteritems():
            y = other_special[attr]
            if not utils_equals(x, y, rtol=rtol, atol=atol, traceback=traceback): 
                if traceback:
                    print("%s: Different %s: %s, %s" %
                          (self.__class__.__name__, attr, repr(x), repr(y)))
                return False
        #--- End: for

        # Check the data
        if self.hasData:
            if other.hasData:
                if not self.Data.equals(other.Data, rtol=rtol, atol=atol,
                                        traceback=traceback):
                    if traceback:
                        print("%s: Different data" % self.__class__.__name__)
                    return False
            else:
                return False
        elif other.hasData:
            return False
        #--- End: if

        return True
    #--- End: def

    def match(self, prop={}, attr={}):
        '''

Determine whether or not a variable satisfies conditions.

Conditions may be specified on the variable's CF properties and
attributes.

:Parameters:

    prop : dict, optional
        Dictionary for which each key/value pair is a CF property name
        and a condition for the property to be tested against. If the
        value is a sequence of conditions then the attribute matches
        if at least one of the conditions is passed.

        In general, a condition is any object and it is passed if the
        attribute is equal to the object, with the following
        exception:

            * If the property is string-valued, then the condition may
              be a regular expression pattern recognised by the `re`
              module and the condition is passed if the property
              matches the regular expression. Special characters for
              the start and end of the string are assumed and need not
              be included. For example, '.*wind' is equivalent to
              '^.*wind$'.

    attr : dict, optional
        Dictionary for which each key/value pair is an attribute name
        and a condition for the attribute to be tested against. If the
        value is a sequence of conditions then the attribute matches
        if at least one of the conditions is passed.

        In general, a condition behaves as for `attr`, with the
        following exception:

            * For the `Units` attribute, the condition is passed if
              the attribute is equivalent (rather than equal) to the
              object. (Note that this behaviour does not apply to the
              `units` attribute.)

:Returns:

    out : bool
        Whether or not the variable matches the given criteria.

**Examples**

'''
        attr = attr.copy()

        units = attr.pop('Units', None)
        if units is not None:
            if not isinstance(units, (tuple, list)):
                units = (units,)

            match = False
            for v in units:
                if isinstance(v, basestring):
                    v = Units(v)

                if v.equivalent(self.Units):
                    match = True
                    break
                else:
                    continue
            #--- End: for
            if not match:
                return False
        #--- End: if

        for x, getfunc in zip(( prop    , attr             ),
                              ('getprop', '__getattribute__')):
            # Loop round criteria
            for key, condition in x.iteritems():

                if condition is None:
                    continue

                try:
                    value = getattr(self, getfunc)(key)
                except AttributeError:
                    # Variable doesn't have this property/attribute
                    return False

                if (isinstance(condition, Comparison) and
                    condition.relation == 'set'):
                    condition = condition.value
                else:
                    condition = (condition,)

                match = False

                for c in condition:
                    if isinstance(c, basestring):
                        match = re_match('^%s$' % c, value)
                    else:
                        match = (value == c)
                        if hasattr(match, 'all') and callable(match.all):
                            match = match.all()
                    #--- End: if                            

                    if match:
                        break
                    else:
                        continue
                #--- End: for

                if not match:
                    return False
            #--- End: for
        #--- End: for

        return True
    #--- End: def

    # ----------------------------------------------------------------
    # Attribute: properties (read only)
    # ----------------------------------------------------------------
    @property
    def mask(self):
        '''

The mask of the data array.

**Examples**

>>> f.shape
(12, 73, 96)
>>> m = f.mask
<CF Field: mask()>
>>> m.shape
(12, 73, 96)
>>> m.dtype
dtype('bool')

'''
        mask = type(self)()

        mask.long_name = 'mask'
        mask.source   = self.name()

        mask.space = self.space.copy()

        mask.Data = self.Data.mask

        return mask
    #--- End: def
    @mask.setter
    def mask(self, value):
        raise AttributeError(
"Can't set %s attribute 'mask': Use 'setmask' to set the data array mask" %
self.__class__.__name__)
    #--- End: def
    @mask.deleter
    def mask(self):
        raise AttributeError("Can't delete %s attribute 'mask'" %
                             self.__class__.__name__)
    #--- End: def

    # ----------------------------------------------------------------
    # Attribute: properties (read only)
    # ----------------------------------------------------------------
    @property
    def properties(self):
        '''

A dictionary of the CF properties.

Note that modifying the returned dictionary will not change the CF
properties.

**Examples**

>>> f.properties
{'_FillValue': 1e+20,
 'foo': 'bar',
 'long_name': 'Surface Air Temperature',
 'standard_name': 'air_temperature',
 'units': 'K'}

'''
        out = {}

        # Start with special properties
        for attr in self._special_attributes:
            if hasattr(self, attr):
                out[attr] = getattr(self, attr)

        # Add simple properties
        out.update(self._simple_attributes())

        return out
    #--- End: def

    def flip(self, axes=None):
        '''

Flip dimensions of the data array in place.

:Parameters:

    axes : int or sequence of ints
        Flip the dimensions whose positions are given. By default all
        dimensions are flipped.

:Returns:

    out : list of ints
        The axes which were flipped, in arbitrary order.

**Examples**

>>> f.flip()
>>> f.flip(1)
>>> f.flip([0, 1])

>>> g = f[::-1, :, ::-1]
>>> f.flip([2, 0]).equals(g)
True

'''
        return self.Data.flip(axes)
    #--- End: def

    def subset(self, *args, **kwargs):
        '''

Return the instance if it matches the given conditions.

Equivalent to:

.. code-block:: python

   def subset(f, *args, **kwargs):
       if f.match(*args, **kwargs):
           return f
       raise ValueError('')

:Parameters:

    args, kwargs :
        As for the `match` method.

:Returns:

    out :
        The variable as an object identity, if it matches the given
        conditions.

:Raises:

    ValueError :
        If the variable does not match the conditions.

'''
        if self.match(*args, **kwargs):
            return self
        else:
            raise ValueError("%s does not match the conditions" %
                             self.__class__.__name__)
    #--- End: def

    def binary_mask(self):
        '''

Return a binary missing data mask of the data array.

The binary mask's data array comprises dimensionless 32-bit integers
and has 0 where the data array has missing data and 1 otherwise.

:Returns:

    out : Variable
        The binary mask.

**Examples**

>>> print f.mask.array
[[ True  False  True False]]
>>> b = f.binary_mask().array
>>> print b.array
[[0 1 0 1]]

'''
        binary_mask = self.copy()

        # Delete all CF properties
        for attr in self.properties:
            self.delprop(attr)

        # Set a long name
        binary_mask.long_name = 'binary_mask'

        binary_mask.Data = self.Data.binary_mask()

        return binary_mask
    #--- End: def

    def expand_dims(self, axis, dim, direction):
        '''
axis is an integer
dim is a string
direction is a boolean

'''
        self.Data.expand_dims(axis, dim, direction)
    #--- End: def

    def setitem(self, value, indices=Ellipsis, condition=None, masked=None,
                other_mask=None, hardmask=None):
        '''

Set selected elements of the data array in place.

The value to which the selected elements of the data array will be set
may be any object which is broadcastable across the selected elements.

Note the following:

* ``f.setitem(value)`` is equivalent to ``f.subspace[...]=value``.

* ``f.setitem(value, indices)`` is equivalent to
  ``f.subspace[indices]=value``.

* If and only if the value to be assigned is the scalar `cf.masked`
  (or `numpy.ma.masked`) then the selected elements of data array's
  *mask* will be set to True (masked). For example,
  ``f.setitem(cf.masked, indices)`` is equivalent to ``f.setmask(True,
  indices)``. This is consistent with the behaviour of numpy masked
  arrays.

:Parameters:

    value : array-like
        The value to which the selected elements of the data array
        will be set. Must be an object which is broadcastable across
        the selected elements.

    indices : optional
        Any indices as would be accepted by ``f.subspace``. Only
        elements of the data array described by the indices, and where
        other criteria (if any) are met, are set to `value`. By
        default, the entire data array is considered. How masked
        elements of the data array are treated depends on the
        `hardmask` parameter. The `value` must be any object
        broadcastable across the shape implied by `indices`. Note that
        ``f.setitem(value, indices)`` is equivalent to
        ``f.subspace[indices]=value``.

    condition : scalar or Comparison, optional
        A condition applied to each element of the data array
        specified by `indices` which determines whether or not that
        element is set to the logically scalar `value`. The condition
        is evaluated by checking if each element equals the condition,
        and if it does then that element is set to `value`. How masked
        elements of the data array are treated depends on the
        `hardmask` parameter. Note that if `condition` is a scalar,
        ``x``, then this is equivalent to the Comparison object
        ``cf.eq(x)``.

    masked : bool, optional
        If False then each unmasked element of the data array
        specified by `indices` is set to the logically scalar
        `value`. If True then each unmasked element of the data array
        specified by `indices` is unmasked and set to the logically
        scalar `value`, regardless of the `hardmask` is parameter.

    other_mask : array-like, optional
        Each element of the data array specified by `indices` and
        which corresponds to an element which evaluates to False from
        `other_mask` is set to the logically scalar
        `value`. `other_mask` may be any object which is broadcastable
        across the shape implied by `indices`. How masked elements of
        the data array are treated depends on the `hardmask`
        parameter.

    hardmask : bool, optional
        If True then any selected elements of the data array which are
        masked *will not* be unmasked and assigned to. If False then
        any selected elements of the data array which are masked
        *will* be unmasked and assigned to. By default, the value of
        the instance's `hardmask` attribute is used.

:Returns:

    None

**Examples**

'''
        self.Data.setitem(value, indices=indices, condition=condition,
                          masked=masked, other_mask=other_mask,
                          hardmask=hardmask)
    #--- End: def

    def setmask(self, value, indices=Ellipsis):
        '''

Set selected elements of the data array's mask in place.

The value to which the selected elements of the mask will be set may
be any object which is broadcastable across the selected elements. The
broadcasted value may be of any data type but will be evaluated as
boolean.

Unmasked elements are set to the fill value.

The mask may be effectively removed by setting every element to False
with ``f.setmask(False)``.

Note that if and only if the value to be assigned is logically scalar
and evaluates to True then ``f.setmask(value, indices)`` is equivalent
to ``f.setitem(cf.masked, indices)``. This is consistent with the
behaviour of numpy masked arrays.

:Parameters:

    value : array-like
        The value to which the selected element s of the mask will be
        set. Must be an object which is broadcastable across the
        selected elements.

    indices : optional
        Indices of the data array. Only elements of the mask described
        by the indices are set to `value`. By default, the entire mask
        is considered.

:Returns:

    None

**Examples**

'''
    	self.Data.setmask(value, indices=indices)
    #--- End: def

    def sin(self):
        '''

Take the trigonometric sine of the data in place.

Units are accounted for in the calculation. For example, the the sine
of 90 degrees_east is 1.0, as is the sine of 1.57079632 radians. If
the units are not equivalent to radians (such as Kelvin) then they are
treated as if they were radians.

The Units are changed to '1' (nondimensionsal).

:Returns:

    None

**Examples**

>>> f.Units
<CF Units: degrees_north>
>>> print f.array
[[-90 0 90 --]]
>>> f.sin()
>>> f.Units
<CF Units: 1>
>>> print f.array
[[-1.0 0.0 1.0 --]]

>>> f.Units
<CF Units: m s-1>
>>> print f.array
[[1 2 3 --]]
>>> f.sin()
>>> f.Units
<CF Units: 1>
>>> print f.array
[[0.841470984808 0.909297426826 0.14112000806 --]]

'''
        self.Data.sin()
    #--- End: def

    def squeeze(self, axes=None):
        '''

Remove size 1 dimensions from the data array in place.

:Parameters:

    axes : int or sequence of ints, optional
        The size 1 axes to remove. By default, all size 1 axes are
        removed. Size 1 axes for removal may be identified by the
        integer positions of dimensions in the data array.

:Returns:

    None

**Examples**

>>> v.squeeze()
>>> v.squeeze(1)
>>> v.squeeze([1, 2])

'''
        return self.Data.squeeze(axes=axes)
    #--- End: def
    
    def transpose(self, axes=None):
        '''

Permute the dimensions of the data array in place.

:Parameters:

    axes : sequence of ints, optional
        The new order of the data array. By default, reverse the
        dimensions' order, otherwise the axes are permuted according
        to the values given. The values of the sequence comprise the
        integer positions of the dimensions in the data array in the
        desired order.

:Returns:

    None

**Examples**

>>> v.transpose()
>>> v.ndim
3
>>> v.transpose([1, 2, 0])

'''
        self.Data.transpose(axes=axes)
    #--- End: def

    def _simple_attributes(self):
        '''

'''
        return self._private['simple_attributes'].copy()
    #--- End: def

    def setprop(self, prop, value):
        '''

Set a CF property.

:Parameters:

    prop : str
        The name of the property to set.

    value :
        The value for the property.

:Returns:

     None

**Examples**

>>> f.setprop('standard_name', 'time')
>>> f.setprop('foo', 12.5)

'''
        # Set a special attribute
        if prop in self._special_attributes:
            setattr(self, prop, value)
            return None
        #--- End: if

        # Still here? Then set a simple attribute
        self._private['simple_attributes'][prop] = value
        
        if prop == 'missing_value':
            self._FillValue = value
    #--- End: def

    def hasprop(self, prop):
        '''

Return True if the variable has a CF property.

:Parameters:

    prop : str
        The name of the property.

:Returns:

     out : bool
         True if the instance has the property.

**Examples**

>>> f.hasprop('standard_name')
True
>>> f.hasprop('foo')
False

'''
        # Has a special property?
        if prop in self._special_attributes:
            return hasattr(self, prop)

        # Still here? Then has a simple property?
        return prop in self._private['simple_attributes']
    #--- End: def

    def identity(self, default=None):
        '''

Return the variable's identity.

The idendity is the value of the `standard_name` property or, if that
does not exist, the `id` attribute.

:Parameters:

    default : optional
        If neither the `standard_name` nor `id` attributes exist then
        return `default`. By default, `default` is None.

:Returns:

    out :
        The identity.

**Examples**

>>> f.standard_name = 'Kelvin'
>>> f.identity
'Kelvin'
>>> del f.standard_name
>>> f.id = 'foo'
>>> f.identity
'foo'
>>> del f.id
>>> f.identity()
None
>>> f.identity('bar')
'bar'

'''
        return getattr(self, 'standard_name', getattr(self, 'id', default))
    #--- End: def

    def getprop(self, prop, *default):
        '''

Get a CF property.

When a default argument is given, it is returned when the attribute
doesn't exist; without it, an exception is raised in that case.

:Parameters:

    prop : str
        The name of the property to get.

    default : optional
        Return `default` if and only if the variable does not have the
        named property.

:Returns:

    out :
        The value of the named property, or the default value.

:Raises:

    AttributeError :
        If the variable does not have the named property and a default
        value has not been set.

**Examples**

>>> f.getprop('standard_name')
>>> f.getprop('standard_name', None)
>>> f.getprop('foo')
AttributeError: Field doesn't have CF property 'foo'
>>> f.getprop('foo', 'bar')
'bar'

'''
        # Get a special attribute
        if prop in self._special_attributes:
            return getattr(self, prop, *default)

        # Still here? Then get a simple attribute
        if prop in self._private['simple_attributes']:
            return self._private['simple_attributes'][prop]
        elif default:
            return default[0]

        # Still here?
        raise AttributeError("%s doesn't have CF property '%s'" %
                             (self.__class__.__name__, prop))
    #--- End: def

    def delprop(self, prop):
        '''

Delete a CF property.

:Parameters:

    prop : str
        The name of the property to delete.

:Returns:

     None

:Raises:

    AttributeError :
        If the variable does not have the named CF property.

**Examples**

>>> f.delprop('standard_name')
>>> f.delprop('foo')
AttributeError: Field doesn't have CF property 'foo'

'''
        # Delete a special attribute
        if prop in self._special_attributes:
            delattr(self, prop)
            return None

        # Still here? Then delete a simple attribute
        if prop in self._private['simple_attributes']:
            del self._private['simple_attributes'][prop]
        else:
            raise AttributeError("%s doesn't have CF property '%s'" %
                                 (self.__class__.__name__, prop))
    #--- End: def

    def name(self, long_name=False, ncvar=False, default=None):
        '''

Return a name for the variable.

Returns the standard_name, long_name (if requested) or netCDF variable
name (if requested), whichever it finds first, otherwise returns a
default name.

:Parameters:

    long_name : bool, optional

        If True, return the long_name if standard_name does not
        exist.

    ncvar : bool, optional

        If True, return ncvar if neither the standard_name nor
        long_name has already been returned.

    default : str, optional

        Return default if neither standard_name, long_name nor ncvar
        has already been returned.

:Returns:

    name : str
        The name of the variable.

'''
        try:
            return self.standard_name
        except AttributeError:
            pass

        if long_name:
            try:
                return self.long_name
            except AttributeError:
                pass

        if ncvar:
            try:
                return self.ncvar
            except AttributeError:
                pass

        return default
    #--- End: def

    def override_units(self, new_units):
        '''

Override the data array units in place.

Not to be confused with setting the `Units` attribute to units which
are equivalent to the original units.

This is different to setting the `Units` attribute, as the new units
need not be equivalent to the original ones and the data array
elements will not be changed to reflect the new units.

:Parameters:

    new_units : str or Units
        The new units for the data array.

:Returns:

    None

**Examples**

>>> f.Units
<CF Units: hPa>
>>> f.first_datum
100000.0
>>> f.override_units('km')
>>> f.Units
<CF Units: km>
>>> f.first_datum
100000.0
>>> f.override_units(cf.Units('watts'))
>>> f.Units
<CF Units: watts>
>>> f.first_datum
100000.0

'''
        self.Data.override_units(new_units)
    #--- End: def

#--- End: class


# ====================================================================
#
# Variable slice object
#
# ====================================================================

class SubspaceVariable(object):

    __slots__ = ('variable',)

    def __init__(self, variable):
        '''

Set the contained variable.

'''
        self.variable = variable
    #--- End: def

    def __getitem__(self, indices):
        '''
x.__getitem__(indices) <==> x[indices]

'''
        variable = self.variable
        new      = variable.copy()

        if variable.hasData:
            new.Data = variable.Data[indices]

        return new
   #--- End: def

    def __setitem__(self, indices, value):
        '''
x.__setitem__(indices, value) <==> x[indices] = value

'''
        self.variable.setitem(value, indices=indices)
#        self.variable.Data[indices] = value
    #--- End: def

#--- End: class
